#! /usr/bin/env python

import sys, shlex, subprocess
from optparse import OptionParser, OptionGroup
from docutils.writers import latex2e
import re

try:
    import locale
    locale.setlocale(locale.LC_ALL, '')
except:
    pass


class AttributedDict(object):
    def __init__(self):
        object.__setattr__(self, "_items", dict())

    def __getattr__(self, key):
        return self._items.get(key)

    def __delattr__(self, key):
        if key in self._items:
            del self._items[key]
        else:
            object.__delattr__(self, key)

    def __setattr__(self, key, val):
        if hasattr(self, key):
            object.__setattr__(self, key, val)
        else:
            self._items[key] = val


parser = OptionParser(usage="Usage: %prog <command> [options]")
OPT = AttributedDict()

OPT.common = OptionGroup(parser, "Common options")

OPT.docinfo = OptionGroup(parser, "Documentation information")
OPT.docinfo.add_option("-i", "--info", dest="docinfo",
        default=None, metavar="FILE",
        help="Read project information from FILE")


is_index_filename = re.compile(r'^[0-9]{2}-[a-zA-Z].*').match


def str_to_numeric_tuple(s):
    r = []
    for item in s.split("-"):
        try:
            r.append(int(item, 10))
        except ValueError:
            # Tried to convert a non-integer, bail out
            break
    return tuple(r)

def str_tuple_sort(a, b):
    return cmp(str_to_numeric_tuple(a), str_to_numeric_tuple(b))


def optparser(*groups):
    parser.add_option_group(OPT.common)
    [parser.add_option_group(g) for g in groups]
    return parser


class DupRefsRemover(object):
    def __init__(self, out):
        if hasattr(out, "write"):
            self.out = out.write
        elif hasattr(out, "handle_line"):
            self.out = out.handle_line
        else:
            raise ValueError("%r has no write/handle_line" % out)
        self.ref = dict()

    def handle_line(self, line):
        if line.startswith(".. _") and ":" in line:
            r = map(lambda x: x.strip(), line[4:].split(":", 1))
            if r[0] in self.ref:
                # Do some sanity check
                if self.ref[r[0]] != r[1]:
                    raise ValueError("Reference '%s' mismatch (%s - %s)"
                            % (r[0], self.ref[r[0]], r[1]))
            else:
                # Add reference and print it out
                self.ref[r[0]] = r[1]
                self.out(line)
        else:
            self.out(line)


class UnderlineXlate(object):
    _XLATE = {
        "#": "=",
        "=": "-",
        "-": "~",
        "~": "^",
    }

    def __init__(self, out):
        if hasattr(out, "write"):
            self.out = out.write
        elif hasattr(out, "handle_line"):
            self.out = out.handle_line
        else:
            raise ValueError("%r has no write/handle_line" % out)

    def handle_line(self, line):
        sline = line.strip()
        if len(sline):
            if sline[0] in self._XLATE:
                sline = sline.replace(sline[0], self._XLATE[sline[0]])
                self.out(sline)
                return
        self.out(line)



class ShellEscapeRunner(object):
    def __init__(self, out):
        self.out = out

    def _expand(self, cmdline):
        cmd = shlex.split(cmdline, False)
        pipe = subprocess.Popen(cmd, stdout=subprocess.PIPE)
        return "\n  ".join(pipe.communicate()[0].rstrip().splitlines())


    def handle_line(self, line):
        index = line.find("!!")
        if index < 0:
            self.out.write(line)
            return

        self.out.write(line[:index].replace("\\!", "!"))
        cmd = line[index+2:]
        end = cmd.find("!!")
        if end > index:
            self.out.write(self._expand(cmd[:end].replace("\\!", "!")))
            self.out.write(cmd[end+2:].replace("\\!", "!"))
        else:
            self.out.write(self._expand(cmd.replace("\\!", "!")))
            self.out.write("\n")



LATEX_SETTINGS = {
    "documentclass"      : "scrbook",
    "documentoptions"    : "11pt,oneside,a4paper",
    "table_style"        : "booktabs",
    "docutils_footnotes" : 1,
    "use_latex_citations": 1,
    "use_latex_toc"      : 1,
    "use_latex_docinfo"  : 0,
    "use_latex_abstract" : 1,
}

EBOOK_SETTINGS = dict(LATEX_SETTINGS)
EBOOK_SETTINGS["documentoptions"] = "14pt,oneside,b5paper"

HTML_SETTINGS = {
    "field_name_limit"     : 20,
    "cloak_email_addresses": 1,
}


# Those are two mocks needed to properly reuse the LaTeX code generator, but
# avoiding generation of all the cruft from the preamble and the postamble,
# and only the middle part.
#
class LaTeXStandaloneTranslator(latex2e.LaTeXTranslator):
    def __init__(self, document):
        self._class = document.settings.documentclass
        self._typearea = ""

        latex2e.LaTeXTranslator.__init__(self, document)

        # XXX We rely on DocumentClass having a document_class attribute and
        #     on it not changing the attribute once initialization was done
        #
        if self.d_class.document_class == "igaliabk":
            self.d_class.document_class = "book"

        try:
            del self.head_prefix[self.head_prefix.index(self.typearea)]
        except AttributeError:
            pass

    def __get_typearea(self):
        if self._class == "igaliabk" or self._class.startswith("scr"):
            value = ""
        else:
            value = self._typearea
        return value

    def __set_typearea(self, value):
        self._typearea = value

    typarea = property(__get_typearea, __set_typearea)



class LaTeXInsertTranslator(LaTeXStandaloneTranslator):
    def astext(self):
        return "".join(self.body)

class LaTeXCustomWriter(latex2e.Writer):
    def __init__(self, translator_class):
        latex2e.Writer.__init__(self)
        self.translator_class = translator_class



def docutils_bug_workaround(acc, x):
    """

    Workarounds this bug: http://sourceforge.net/p/docutils/bugs/215/
    """
    problematic_parameter = '--stylesheet-path'
    if str.find(x, problematic_parameter) == -1:
        acc.append(x)
    else:
        acc.append(problematic_parameter)
        acc.append([x[str.find(x,'=') + 1:]])
    return acc

class DocTool(object):

    def main(self, argv=None):
        if argv is None:
            argv = sys.argv

        if len(argv) < 2:
            raise SystemExit("Insufficient number of arguments")

        method = getattr(self, "cmd_" + argv[1], self.no_command)
        method(*argv[1:])

    def cmd_rstinsert(self, cmd, *args):
        from docutils.core import publish_cmdline, default_description
        description = (
            "Generates LaTeX portions to be included in documents " +
            "from standalone reStructuredText sourtces " +
            default_description)

        publish_cmdline(writer=LaTeXCustomWriter(LaTeXInsertTranslator),
                description=description,
                argv=list(args)
                )

    def cmd_rst2latex(self, cmd, *args):
        from docutils.core import publish_cmdline, default_description
        description = (
            'Generates LaTeX documents from RST sources. ' +
            default_description)

        publish_cmdline(writer=LaTeXCustomWriter(LaTeXStandaloneTranslator),
                settings_overrides=LATEX_SETTINGS,
                description=description,
                argv=list(args),
                )

    def cmd_rst2ebook(self, cmd, *args):
        from docutils.core import publish_cmdline, default_description
        description = (
            'Generates LaTeX documents from RST sources. ' +
            default_description)

        publish_cmdline(writer=LaTeXCustomWriter(LaTeXStandaloneTranslator),
                settings_overrides=EBOOK_SETTINGS,
                description=description,
                argv=list(args),
                )

    def cmd_rst2html(self, cmd, *args):
        from docutils.core import publish_cmdline, default_description
        description = (
            'Generates HTML documents from RST sources. ' +
            default_description)

        publish_cmdline(writer_name="html",
                settings_overrides=HTML_SETTINGS,
                description=description,
                argv=reduce(docutils_bug_workaround, list(args), []),
                )

    def cmd_toplevel(self, cmd, *args):
        opts, args = optparser(OPT.docinfo).parse_args(list(args))

        if opts.docinfo:
            f = file(opts.docinfo, "rU")
            e = ShellEscapeRunner(sys.stdout)
            map(e.handle_line, f.readlines())
            f.close()
            print

        print ".. contents::"
        print

        out = DupRefsRemover(sys.stdout)
        out_xlate = UnderlineXlate(out)

        args = list(args)
        args.sort(str_tuple_sort)

        for name in args:
            if not name.endswith(".rst"):
                continue

            f = file(name, "rU")
            map(is_index_filename(name) and out.handle_line or out_xlate.handle_line,
                    filter(lambda l: ".. contents::" not in l,
                        f.readlines())
                    )
            f.close()
            print


    def cmd_htmlindex(self, cmd, *args):
        opts, args = optparser(OPT.docinfo).parse_args(list(args))

        if opts.docinfo:
            f = file(opts.docinfo, "rU")
            e = ShellEscapeRunner(sys.stdout)
            map(e.handle_line, f.readlines())
            f.close()
            print

        args = list(args)
        args.sort(str_tuple_sort)

        for name in args:
            # Skip non-RST inputs
            if not name.endswith(".rst") or not is_index_filename(name):
                continue

            f = file(name, "rU")
            line = f.readline().strip()
            f.close()
            print "#. `%s <%s.html>`__" % (line, name[:-4])


    def cmd_help(self, cmd, *args):
        if args:
            self.main([args[0], args[0], "--help"])
        else:
            print "Available commands:"
            for k in dir(self):
                if k.startswith("cmd_"):
                    print " ", k[4:].replace("_", "-")

    cmd_commands = cmd_help

    def no_command(self, cmd, *args):
        raise SystemExit("No such command '%s'" % cmd)



if __name__ == "__main__":
    DocTool().main()

